using System;

namespace Instedd.Labs.Geo.Library.Kml
{
    public class LatLong
    {
        // TODO: Ride of this ctor and use the Parse(...) methods.
        public LatLong(String latLong)
        {
            String []parts = latLong.Split(' ');
            if (parts.Length != 2)
            {
                throw new Exception(String.Format("Invalid LatLong specified: {0}",latLong));
            }
            this.latitude = Double.Parse(parts[0]);
            this.longitude = Double.Parse(parts[1]);

            CheckLatLongIsInRange(this.latitude, this.longitude);

            this.alt = 0;
        }

        public LatLong(double lat, double longitude)
            : this(lat, longitude, 0)
        { }

        public LatLong(double latitude, double longitude, double alt)
        {
            this.latitude = latitude;
            this.longitude = longitude;
            CheckLatLongIsInRange(this.latitude, this.longitude);
        }

        void CheckLatLongIsInRange(double latitude, double longitude)
        {
            if (latitude < -90 || latitude > 90)
            {
                throw new ArgumentOutOfRangeException("latitude");
            }

            if (longitude < -180 || latitude > 180)
            {
                throw new ArgumentOutOfRangeException("longitude");
            }
        }

        private double latitude;

        public double Lat
        {
            get { return latitude; }
            set
            {
                CheckLatLongIsInRange(value, this.longitude);
                this.latitude = value;
            }
        }

        private double longitude;

        public double Long
        {
            get { return longitude; }
            set
            {
                CheckLatLongIsInRange(this.latitude, value);
                this.longitude = value;
            }
        }

        private double alt;

        public double Alt
        {
            get { return alt; }
        }

        public override bool Equals(object obj)
        {
            LatLong anotherLatLong = obj as LatLong;
            if (anotherLatLong != null)
            {
                return this.latitude == anotherLatLong.latitude && this.longitude == anotherLatLong.longitude;
            }

            return false;
        }

        public override int GetHashCode()
        {
            return this.latitude.GetHashCode() + this.longitude.GetHashCode();
        }

        void DecimalToDMS(double dec, out double degrees, out double minutes, out double seconds)
        {
            int sign=1;

            if (dec < 0)
            {
               sign = -1;
            }
            dec = Math.Abs(Math.Round(dec*1000000));
            degrees = Math.Floor(dec/1000000) * sign;
            minutes = Math.Floor(((dec / 1000000) - Math.Floor(dec / 1000000)) * 60);
            seconds = (Math.Floor(((((dec / 1000000) - Math.Floor(dec / 1000000)) * 60) - Math.Floor(((dec / 1000000) - Math.Floor(dec / 1000000)) * 60)) * 100000) * 60 / 100000);
        }

        public String ToString(String format)
        {
            format = format.ToLower();
            if (format == "dms")
            {
                double degreesLat, minutesLat, secondsLat;
                double degreesLong, minutesLong, secondsLong;
                DecimalToDMS(latitude, out degreesLat, out minutesLat, out secondsLat);
                DecimalToDMS(longitude, out degreesLong, out minutesLong, out secondsLong);
                return String.Format("Lat: {0}\xB0 {1}' {2}'' Long: {3}\xB0 {4}' {5}''", degreesLat, minutesLat, secondsLat, degreesLong, minutesLong, secondsLong);
            }
            else if (format=="decimal")
            {
                return String.Format("Lat: {0} Long: {1}", latitude, longitude);
            }
            
            return ToString();
        }

        public override string ToString()
        {
            return String.Format("{0} {1}", latitude, longitude);
        }

        const char LatLongDefaultSeparator = ' ';

        public static LatLong Parse(string latLongString)
        {
            return Parse(latLongString, LatLongDefaultSeparator);
        }

        public static LatLong Parse(string latLongString, char latLongSeparator)
        {
            String[] parts = latLongString.Split(latLongSeparator);
            if (parts.Length != 2)
            {
                return null;
            }
            return Parse(parts[0], parts[1]);
        }

        public static LatLong Parse(string latitude, string longitude)
        {
            LatLong latLong = new LatLong(ParseLatLongComponent(latitude), ParseLatLongComponent(longitude));
            return latLong;
        }

        /// <summary>
        /// Parses a latitude or longitude value.
        /// The string value could be in both DMS and Decimal formats.
        /// </summary>
        /// <param name="component"></param>
        /// <returns></returns>
        private static double ParseLatLongComponent(string component)
        {
            string[] componentParts = component.Split('.');

            if (componentParts.Length > 2)
            {
                // DMS component value

                // TODO: Check how the sign will be included in the compoenent string

                double sign = ParseLatLongComponent(componentParts[0]) >= 0 ? 1 : -1;
                double degrees = Math.Abs(Math.Round(ParseLatLongComponent(componentParts[0]) * 1000000d));
                double minutes = Math.Abs(Math.Round(ParseLatLongComponent(componentParts[1]) * 1000000d));
                double seconds = double.MinValue;
                if (componentParts.Length == 3)
                {
                    seconds = Math.Abs(Math.Round(ParseLatLongComponent(componentParts[2]) * 1000000d));
                }
                else
                {
                    seconds = Math.Abs(Math.Round(ParseLatLongComponent(string.Join(".", new string[] { componentParts[2], componentParts[3] })) * 1000000d));
                }

                return Math.Round(degrees + (minutes / 60d) + (seconds / 3600d)) * sign / 1000000d;
            }
            else
            {
                // Decimal component value
                return double.Parse(component);
            }
        }

        public static bool TryParse(string latLongString, out LatLong latLong)
        {
            latLong = null;
            
            try
            {
                latLong = Parse(latLongString);
            }
            catch
            {
                return false;
            }

            return latLong != null;
        }
    }
}